package com.xucia.jsponic.datasource;

import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.hsqldb.HSQLClientConnection;
import org.json.JSONObject;

import com.xucia.jsponic.data.JsonPath;
import com.xucia.jsponic.util.StringEncoder;
/**
 * 
 * @author Kris Zyp
 * This provides a default starting implementation for JDBC database sources
 */
public abstract class DatabaseDataSource extends BaseDataSource {
	public static interface DatabaseAction {
		public Object execute() throws SQLException;
	}
	public static abstract class ThreadSpecificConnectionObject {
		protected Connection connection;
		public Connection getConnection() {
			return connection;
		}
		public void setConnection(Connection connection) {
			this.connection = connection;
		}
	}
	protected JsonPath parseIfQuery(String query){
		if (query.matches("([^\\/]*\\/)?select.*")) // TODO: make this static and shared
			return JsonPath.idForString(query);		
		return null;
	}
	String username = "sa";
	String password = "";
	String driverClassName;
    public void initParameters(JSONObject parameters) throws Exception {
    	if (parameters.has("connection"))
    		this.connectionString = parameters.getString("connection");
    	if (parameters.has("username"))
    		this.username = parameters.getString("username");
    	if (parameters.has("password"))
    		this.password = parameters.getString("password");
    	if (parameters.has("driver"))
    		driverClassName = parameters.getString("driver");
    	Class.forName(driverClassName).newInstance();
    	if (parameters.has("characterSet")) {
	    	this.needConversion = true;
	    	this.characterSet = parameters.getString("characterSet");
    	}
	}
    protected String connectionString;
    /** 
     * This method will be called each time a new connection is needed, 
     * implementators can store prepared statements along with the connection. 
     * Each connection object is thread specific, it should not be shared across 
     * sessions and accessing it through getConnectionObject will keep it thread specific.
     * @param connection
     * @return
     */
    abstract ThreadSpecificConnectionObject setupConnection(Connection connection) throws SQLException;
    Map<Thread,ThreadSpecificConnectionObject> threadMap = new HashMap();
    
    /**
     * This will return a thread specific connection object. It will create a new one if necessary
     * @return
     * @throws SQLException
     */
    protected ThreadSpecificConnectionObject getConnectionObject() {
    	return getConnectionObject(false);
    }
    protected Connection createConnection() throws SQLException {
    	while (true) {
	    	try {
	    		return DriverManager.getConnection(connectionString,username,password);
	    		/* TODO: Do this if it is mysql: 
	    		 *         ResultSet rs = executeQuery("show variables like \"character_set_database\"");
        if (rs.next())
        	characterSet = rs.getString(2);        
        rs.close();
        needConversion = !("UTF8".equals(characterSet.toUpperCase()));
		return super.setupConnection(connection);

	    		 */
	    	}
	    	catch (SQLException e) { // sometimes the process is still in use from last time it was run, so if wait a sec, it should become available
	    		if (e.getMessage().indexOf("already in use") != -1) {
	    			System.err.println(e.getMessage());
	    			System.err.println("Will wait to see if it becomes available");
	    			try {
						Thread.sleep(1000);
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
	    		}
	    		else
	    			throw e;
	    	}
    	}
    	
    }
    ThreadSpecificConnectionObject getConnectionObject(boolean forceNew) {
    	ThreadSpecificConnectionObject connectionObject = threadMap.get(Thread.currentThread());
    	if (connectionObject == null || forceNew) {
        	try {
        		Connection connection = createConnection(); 
        		connectionObject = setupConnection(connection);
        		connectionObject.setConnection(connection);
        	}
        	catch (SQLException e) {
        		throw new RuntimeException(e);
        	}
    		threadMap.put(Thread.currentThread(), connectionObject);
    	}
    	return connectionObject;
    }
	/**
	 * This will attempt to perform the given action. If it fails, it will try to create a new connection and try again.
	 * @param action
	 * @throws SQLException
	 */
	public Object tryExecution(DatabaseAction action) throws SQLException {
		try {
			return action.execute();
		}
		catch (SQLException e) {
			getConnectionObject(true);
			return action.execute();
		}
	}
	public long executeAndGetGeneratedKey(PreparedStatement statement) throws SQLException {
		ResultSet rs;
		if (getConnectionObject().getConnection() instanceof org.hsqldb.jdbc.jdbcConnection) {
			statement.execute();
	    	statement = getConnectionObject().getConnection().prepareStatement("CALL IdENTITY()");
	        rs = statement.executeQuery();
		}
		else {
			rs = statement.getGeneratedKeys();
		}
        long key = -1;
        if (rs.next()) 
            key = rs.getLong(1); 
        rs.close();
        return key;
	}
    static StringEncoder stringEncoder = new StringEncoder();
    static {
        stringEncoder.setUnsafeStrings(new String[] {"'"},new String[] {"''"});
    }
    String makeStringSQLSafe(String str) {
		str = encodeString(stringEncoder.encode(str));
    	return str;
    } 
    String characterSet = "latin1";
	boolean needConversion = false;
    String decodeString(String value) {
    	if (needConversion)
			try {
				value = new String(value.getBytes(characterSet),"UTF-8");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		return value;
    }
    String encodeString(String value) {
    	if (needConversion)
			try {
				value = new String(value.getBytes("UTF-8"),characterSet);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		return value;
    }
}















